/**
 * 
 */
package br.com.framework.persistencia.impl;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.apache.log4j.Logger;

import br.com.framework.persistencia.api.BaseDao;
import br.com.framework.persistencia.model.AuditedEntity;
import br.com.framework.persistencia.model.AuditedEntity.Status;
import br.com.framework.persistencia.model.BaseEntity;
import br.com.framework.persistencia.qualifier.DataRepository;
import br.com.framework.persistencia.sort.Order;

/**
 * Implementação abstrata da interface no padrão DAO (Data Access Object) que permite manipular os dados de uma entidade.
 * 
 * @author Cleber Moura
 *
 * @param <ID> representa o tipo do identificador da entidade.
 * @param <E> representa o tipo da entidade.
 */
// Indica que o gerenciamento das trasações será realizado pelo conteiner
@TransactionManagement(TransactionManagementType.CONTAINER)
// Indica que os métodos dessa classe utilizam a transação existente ou cria uma nova caso não exista.
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public abstract class BaseDaoImpl<E extends BaseEntity> implements BaseDao<E> {
	
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	protected static Logger LOGGER;

	private Class<E> entityClass;

	@Inject
	@DataRepository
	private EntityManager entityManager;
	
	/**
	 * Construtor que recebe a entidade como parâmetro.
	 * @param clazz
	 */
	public BaseDaoImpl(Class<E> clazz) {
		this.entityClass = clazz;
		LOGGER = Logger.getLogger(getClass());
	}
	
	/**
	 * Retorna a classe da entidade.
	 * @return
	 */
	protected Class<E> getEntityClass() {
		return entityClass;
	}
	
	/**
	 * @return
	 */
	protected String getEntityClassName() {
		return entityClass.getSimpleName();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public E getByID(Long id) throws PersistenceException {
		return entityManager.find(entityClass, id);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public List<E> findAll() throws PersistenceException {
		return findAll(true);
	}
	/**
	 * {@inheritDoc}
	 */
	public List<E> findAll(boolean onlyActive) throws PersistenceException {
		return findAll(-1, -1, null, null, null, onlyActive);
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public List<E> findAll(int firstResult, int maxResults,
			String sortField, Order sortOrder, Map<String, String> filters,
			boolean onlyActive) throws PersistenceException {
		Query query = createQueryBuscarTodos(false, filters, sortField, sortOrder, onlyActive);
	    if (firstResult > -1 && maxResults > -1){
	    	query.setFirstResult(firstResult);
	    	query.setMaxResults(maxResults);
		}
		return (List<E>) query.getResultList();
	}
	
	
	/**
	 * Retorna o {@link Field} correspondente ao fieldName informado
	 * 
	 * @param fieldName
	 * @return
	 */
	private Field getField(String fieldName, Class<?> clazz) {
		Field field = null;
		while (BaseEntity.class.isAssignableFrom(clazz)) {
			field = null;
			try {
				field = clazz.getDeclaredField(fieldName);
			} catch (Exception e) {
				// nada a fazer
			}
			if (field != null) {
				break;
			}
			clazz = clazz.getSuperclass();
		}
		return field;
	}
	
	/**
	 * @param clazz
	 * @return
	 */
	private Field[] getAllFields(Class<?> clazz) {
		List<Field> fieldList = new ArrayList<Field>();
		while (BaseEntity.class.isAssignableFrom(clazz)) {
			Field[] fields = clazz.getDeclaredFields();
			if (fields != null && fields.length > 0) {
				fieldList.addAll(Arrays.asList(fields));
			}
			clazz = clazz.getSuperclass();
		}
		Field[] fieldResult = new Field[fieldList.size()];
		return fieldList.toArray(fieldResult);
	}
	
	
	/**
	 * Constroi a query de busca de todos os registros.
	 * 
	 * @param qryStr
	 * @param filters
	 * @param sortField
	 * @param sortOrder
	 * @param onlyActive
	 * @return
	 */
	private Query createQueryBuscarTodos(boolean isQueryCount, Map<String, String> filters, String sortField, Order sortOrder, boolean onlyActive) {
		StringBuffer qryStr = new StringBuffer();
		if (isQueryCount) {
			qryStr.append("SELECT COUNT(e) FROM ");
		} else {
			qryStr.append("SELECT e FROM ");
		}
		qryStr.append(getEntityClassName());
		qryStr.append(" AS e WHERE (1=1) ");
		if (onlyActive && AuditedEntity.class.isAssignableFrom(getEntityClass())) {
	    	qryStr.append("AND e.status = :status ");
	    }
		
		if (filters != null && !filters.isEmpty()) {
			for (Entry<String, String> entry : filters.entrySet()) {
				String fieldName = entry.getKey();
				Field field = getField(fieldName, getEntityClass());
				if (field != null) {
					if (String.class.isAssignableFrom(field.getType())) {
						qryStr.append("AND LOWER(e." + fieldName + ") LIKE LOWER(:"+ fieldName +") ");
					} else {
						qryStr.append("AND e." + fieldName + " = "+ entry.getValue() +" ");
					}
				} else {
					throw new PersistenceException("O atributo "+entry.getKey()+" não pode ser utilizado para o filtro.");
				}
			}
		}
		if (sortField != null && !sortField.trim().isEmpty()) {
			qryStr.append("ORDER BY e.");
			qryStr.append(sortField + " " + sortOrder.getOrder());
		}
	    Query query = entityManager.createQuery(qryStr.toString());
	    if (onlyActive && AuditedEntity.class.isAssignableFrom(getEntityClass())) {
	    	query.setParameter("status", Status.ACTIVE);
	    }
	    if (filters != null && !filters.isEmpty()) {
			for (Entry<String, String> entry : filters.entrySet()) {
				String fieldName = entry.getKey();
				Field field = getField(fieldName, getEntityClass());
				if (String.class.isAssignableFrom(field.getType())) {
					query.setParameter(entry.getKey(), "%" + entry.getValue() + "%");
				}
			}
	    }
	    return query;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public Long getRecordCount() throws PersistenceException {
		return getRecordCount(true);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public Long getRecordCount(boolean onlyActive) throws PersistenceException {
		return getRecordCount(null, onlyActive);
	}
	
	@Override
	public Long getRecordCount(Map<String, String> filters,
			boolean onlyActive) throws PersistenceException {
		Query query = createQueryBuscarTodos(true, filters, null, null, onlyActive);
		return (Long) query.getSingleResult();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public void insert(E entidade) throws PersistenceException {
		entityManager.persist(entidade);
	}
	/**
	 * {@inheritDoc}
	 */
	public void update(E entidade) throws PersistenceException {
		entityManager.merge(entidade);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public void refresh(E entidade) throws PersistenceException {
		entityManager.refresh(entidade);
	}
	/**
	 * {@inheritDoc}
	 */
	public void remove(E entidade) throws PersistenceException {
		remove(entidade, true);
	}
	/**
	 * {@inheritDoc}
	 */
	public void remove(E entidade, boolean removerDefinitivamente) throws PersistenceException {
		if ((entidade instanceof AuditedEntity) && !removerDefinitivamente) {
			((AuditedEntity)entidade).setStatus(Status.INACTIVE);
			this.update(entidade);
		} else {
			if (!entityManager.contains(entidade)) {
				entidade = entityManager.merge(entidade);
			}
			entityManager.remove(entidade);
		}
	}
	/**
	 * {@inheritDoc}
	 */
	public void remove(Long id) throws PersistenceException {
		remove(id, true);
		
	}
	/**
	 * {@inheritDoc}
	 */
	public void remove(Long id, boolean removerDefinitivamente) throws PersistenceException {
		E entidade = getByID(id);
		if (entidade != null) {
			remove(entidade, removerDefinitivamente);
		} else {
			throw new EntityNotFoundException("Entidade/ID: " + getEntityClass().getSimpleName() + "/" + id); 
		}
	}
	/**
	 * {@inheritDoc}
	 */
	public List<E> buscarPorCriteria(CriteriaQuery<E> criteriaQuery)
			throws PersistenceException {
		TypedQuery<E> tQuery = entityManager.createQuery(criteriaQuery);
		return tQuery.getResultList();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public List<E> findByExample(E entidade, int firstResult, int maxResults,
			String sortField, Order sortOrder, boolean isStringCaseSensitive, boolean isLikeEnabled) throws PersistenceException{
		
		CriteriaBuilder cBuilder = getEntityManager().getCriteriaBuilder();
		CriteriaQuery<E> query = cBuilder.createQuery(getEntityClass());
		Root<E> from = query.from(getEntityClass());
		Field[] fields = getAllFields(entidade.getClass());
		List<Predicate> predicates = new ArrayList<Predicate>();
		for (int i = 0; i < fields.length; i++) {
			Field field = fields[i];
			if (!Modifier.isStatic(field.getModifiers())) {
				Object value = null;
				try {
					if (!field.isAccessible()) {
						field.setAccessible(true);
					}
					value = field.get(entidade);
				} catch (Exception e) {
					LOGGER.error("Erro ao obter valor para Entidade/Field = " 
							+ getEntityClassName() + "/" + field.getName(), e);
				}
				if (value != null) {
					Predicate predicate = null;
					if (Collection.class.isAssignableFrom(field.getType())){
						Class<?> elementType = (Class<?>) ((ParameterizedType)field.getGenericType()).getActualTypeArguments()[0];
						Collection<?> elements = (Collection<?>) value;
						for (Object element : elements) {
							Expression<?> joinAs = from.join(field.getName(), JoinType.INNER).as(elementType);
							cBuilder.equal(joinAs, element);
							predicate = cBuilder.equal(joinAs, element);
							predicates.add(predicate);
						}
					} else if (String.class.isAssignableFrom(field.getType())) {
						Path<String> path = from.get(field.getName());
						if (isLikeEnabled) {
							if (isStringCaseSensitive) {
								predicate = cBuilder.like(path, (String)value);
							} else {
								predicate = cBuilder.like(cBuilder.lower(path), ((String)value).toLowerCase());
							}
						} else {
							if (isStringCaseSensitive) {
								predicate = cBuilder.equal(path, (String)value);
							} else {
								predicate = cBuilder.equal(cBuilder.lower(path), ((String)value).toLowerCase());
							}
						}
						predicates.add(predicate);
					} else {
						predicate = cBuilder.equal(from.get(field.getName()), value);
						predicates.add(predicate);
					}
				}
			}
		}
		Predicate[] predicateArray = new Predicate[predicates.size()];
		query.where(predicates.toArray(predicateArray));
		
		if (sortField != null && !sortField.trim().isEmpty()) {
			if (sortOrder.equals(Order.ASCENDING)) {
				query.orderBy(cBuilder.asc(from.get(sortField)));
			} else {
				query.orderBy(cBuilder.desc(from.get(sortField)));
			}
		}
		
		TypedQuery<E> tQuery = getEntityManager().createQuery(query);
		if (firstResult > -1 && maxResults > -1){
			tQuery.setFirstResult(firstResult);
			tQuery.setMaxResults(maxResults);
		}
		return tQuery.getResultList();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public List<E> findByExample(E entidade, boolean isStringCaseSensitive, boolean isLikeEnabled) throws PersistenceException{
		return findByExample(entidade, -1, -1, null, null, isStringCaseSensitive, isLikeEnabled);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public List<E> findByExample(E entidade, String sortField, Order sortOrder) throws PersistenceException{
		return findByExample(entidade, -1, -1, sortField, sortOrder, true, false);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public List<E> findByExample(E entidade) throws PersistenceException{
		return findByExample(entidade, null, null);
	}

	/**
	 * @return
	 */
	protected EntityManager getEntityManager() {
		return entityManager;
	}
	
	/**
	 * @param entityManager the entityManager to set
	 */
	protected void setEntityManager(EntityManager entityManager) {
		this.entityManager = entityManager;
	}
	
}
